package model;

import domain.RawDataDO;
import dto.DayAcquisitionDTO;
import dto.SinglePointDTO;
import model.data.Point;
import model.mode.PreTreatMode;

import java.util.*;

/**
 * @author 刀氏春秋
 * @date 2023/5/15
 */
public class DataPreTreatUtil {
    /**
     * 完整电流-电压数据
     */
    private List<RawDataDO> dataList;

    /**
     * 整流调整触发阈值
     */
    private final static Double ADJUST_THRESHOLD = 3d;

    private final DataLoadUtil loader = new DataLoadUtil();

    /**
     * 加载行式数据，传入DayAcquisitionDTO数据表，一个数据对象元对应表单相单日电流或电压数据
     * 导入时电流电压数据需分拆成两个列表
     *
     * @param current 电流数据
     * @param voltage 电压数据
     */
    public void loadRowStyleData(List<DayAcquisitionDTO> current, List<DayAcquisitionDTO> voltage) {
        this.dataList = loader.getAcquisitionData(current, voltage);
    }

    /**
     * 加载列式数据，传入SinglePointDTO数据表，一个数据元可存储一个采集点的三相电流电压值
     * 导入数据时，总表和户表需要分开导入
     *
     * @param gateway 总表数据
     * @param user    户表数据
     */
    public void loadColumnStyleData(List<SinglePointDTO> gateway, List<SinglePointDTO> user) {
        this.dataList = loader.getWeekSinglePointData(gateway, user);
    }

    /**
     * 根据不同策略过滤数据
     *
     * @param mode 数据过滤模式
     * @return 过滤后电流-电压-功率数据
     */
    public List<RawDataDO> filterData(PreTreatMode mode) {
        Map<String, List<Integer>> ridIndexByVoltageMap = null;
        List<Integer> ridIndexByPower = null;
        // 根据不同模式获取筛选序列
        switch (mode) {
            case FILTER_BY_GATEWAY_VOLTAGE: {
                ridIndexByVoltageMap = filterWeekDataByGatewayVoltage();
                break;
            }
            // 截至2023/6/15，方天数据集无法通过平均功率筛选
            case FILTER_BY_AVG_POWER: {
                List<Double> dayAvgPower = calculateDayAvgPower(this.dataList.get(0));
                ridIndexByPower = filterWeekDataByAvgPower(dayAvgPower, this.dataList.get(0));
                break;
            }

        }
        // 开始执行筛选
        for (RawDataDO item : this.dataList) {
            // 跳过总表
            if (item.getWiringMode().equals("3")) continue;

            List<Integer> ridPointIndex;
            if (ridIndexByVoltageMap != null) {
                // 如果电压去除点映射不为空
                ridPointIndex = ridIndexByVoltageMap.get(item.getAssetNo());
            } else if (ridIndexByPower != null) {
                // 如果功率去除点映射不为空
                ridPointIndex = ridIndexByPower;
            } else {
                // 两个都为空，或两个都不为空，说明出问题了，此时不执行任何筛选
                continue;
            }
            List<Point> points = item.getPoints();
            for (int i = 0; i < points.size(); i++) {
                // 通过置为空值的方法将其化为null
                if (ridPointIndex.contains(i)) {
                    points.get(i).setCurrent(null);
                    points.get(i).setVoltage(null);
                }
            }
        }
        return this.dataList;
    }

    /**
     * 计算总表每日平均功率
     *
     * @param gatewayData 总表单点功率数据
     * @return 总表每日平均功率数据表
     */
    private List<Double> calculateDayAvgPower(RawDataDO gatewayData) {
        // 首先计算每日平均功率
        List<Double> dayAvgPower = new LinkedList<>();
        List<Point> points = gatewayData.getPoints();
        Double sumPower = 0d;
        for (int i = 0; i < points.size(); i++) {
            // 功率累加
            sumPower += points.get(i).getPower();
            if ((i + 1) % 96 == 0) {
                // 累加满一天后，计算平均功率并清空累加功率
                dayAvgPower.add(sumPower / 96.0d);
                sumPower = 0d;
            }
        }
        return dayAvgPower;
    }

    /**
     * 根据日平均功率过滤数据点
     *
     * @param dayAvgPower 日数据功率阈值
     * @param gatewayData 总表数据
     * @return 去掉的数据点编号列表
     */
    private List<Integer> filterWeekDataByAvgPower(List<Double> dayAvgPower, RawDataDO gatewayData) {
        // 保存取点序号
        List<Integer> indexes = new LinkedList<>();
        List<Point> points = gatewayData.getPoints();
        for (int i = 0; i < points.size(); i++) {
            // 筛选总表数据，每个数据点找到对应的
            if (points.get(i).getPower() < dayAvgPower.get(i / 96)) indexes.add(i);
        }
        return indexes;
    }

    /**
     * 根据总表电压数据筛选用户表数据，有一下几个采点思路
     * 1、只要总表小于全部三相的点
     * 2、总表小于最高两相的点
     */
    private Map<String, List<Integer>> filterWeekDataByGatewayVoltage() {
        // 获取总表数据
        List<RawDataDO> gateWayData = this.dataList.subList(0, 3);

        Map<String, List<Integer>> indexMap = new HashMap<>();
        // 遍历所有户表数据，再找到对应总表点位
        for (int i = 3; i < this.dataList.size(); i++) {
            List<Point> itemData = this.dataList.get(i).getPoints();
            List<Integer> ridIndexes = new LinkedList<>();
            // 若大于三相任意一相电压，则将点加入
            for (int j = 0; j < itemData.size(); j++) {
                if (itemData.get(j).getVoltage() == null) {
                    continue;
                }
                // 大于任意一个电压值，都判定为不正确
                if (gateWayData.get(0).getPoints().get(j).getVoltage() < itemData.get(j).getVoltage() ||
                        gateWayData.get(1).getPoints().get(j).getVoltage() < itemData.get(j).getVoltage() ||
                        gateWayData.get(2).getPoints().get(j).getVoltage() < itemData.get(j).getVoltage())
                    ridIndexes.add(j);
            }
            indexMap.put(this.dataList.get(i).getAssetNo(), ridIndexes);
        }
        return indexMap;
    }

    /**
     * 检查三个点中是否存在空值点
     *
     * @param left  左点
     * @param mid   中点
     * @param right 右点
     * @return true - 存在空值点，false，不存在空值点
     */
    private boolean checkNullPoints(Point left, Point mid, Point right) {
        return left.getCurrent() == null || mid.getCurrent() == null || right.getCurrent() == null;
    }

    /**
     * 代入三个点整流操作，若三点中其中一点平均值大于另两点，则将其替换为另两点的平均值
     *
     * @param left  左点
     * @param mid   中点
     * @param right 右点
     * @return 返回需得以修改的点编号，0代表左，1代表中，2代表右，无需修改返回-1
     */
    private int rectify(Point left, Point mid, Point right) {
        // 如果存在空值，直接跳过
        if (checkNullPoints(left, mid, right)) {
            return -1;
        }
        // 主要检查电流，暂时跳过电压筛查
        Double avgLM = (left.getCurrent() + mid.getCurrent()) * 0.5d;
        Double avgMR = (mid.getCurrent() + right.getCurrent()) * 0.5d;
        Double avgLR = (left.getCurrent() + right.getCurrent()) * 0.5d;

        if (right.getCurrent() > avgLM * ADJUST_THRESHOLD) {
            // 如果右值远大于左和中的平均值，识别其为噪点，将其置换为均值
            right.setCurrent(avgLM);
            return 1;
        } else if (left.getCurrent() > avgMR * ADJUST_THRESHOLD) {
            // 如果左值远大于右和中的平均值，识别其为噪点，将其置换为均值
            left.setCurrent(avgMR);
            return 0;
        } else if (mid.getCurrent() > avgLR * ADJUST_THRESHOLD) {
            // 如果中值远大于左和右的平均值，识别其为噪点，将其置换为均值
            mid.setCurrent(avgLR);
            return 2;
        }
        // 其他情况，代表没有太大的数值波动，此时无需任何处理
        return -1;
    }

    /**
     * 整流电表数据，平滑噪点，不删点
     *
     * @param list 调整目标数据表
     */
    public void adjustPointsToRidNoise(List<RawDataDO> list) {
        // 三人委员会 + 滑动窗口
        // 三人委员会用以判断是否需要做出噪点调整，如果三个点中其中一点的平均值远大于另外两点，表明其为噪点，需要调整
        for (RawDataDO item : list) {
            if (item.getWiringMode().equals("3")) continue;
            // 滑动窗口起始点
            int leftFrame = 0;
            int rightFrame = 2;
            while (rightFrame < item.getPoints().size()) {
                // 分别代入左点中点和右点，需要需要调整的点
                rectify(item.getPoints().get(leftFrame), item.getPoints().get(++leftFrame), item.getPoints().get(rightFrame++));
            }
        }
    }

    /**
     * 数据标准化操作
     *
     * @param item 单相表数据
     */
    private void normalize(RawDataDO item) {
        // 首先算出均值
        Double avgVoltage = 0d;
        Double avgCurrent = 0d;

        for (Point point : item.getPoints()) {
            if (point.getCurrent() != null && point.getVoltage() != null) {
                avgVoltage += point.getVoltage();
                avgCurrent += point.getCurrent();

            }
        }
        avgVoltage /= item.getPoints().size();
        avgCurrent /= item.getPoints().size();

        // 去均值化
        Double maxVoltage = 0d;
        Double maxCurrent = 0d;
        for (Point point : item.getPoints()) {
            if (point.getCurrent() != null && point.getVoltage() != null) {
                point.setVoltage(point.getVoltage() - avgVoltage);
                point.setCurrent(point.getCurrent() - avgCurrent);
                if (maxCurrent < point.getCurrent()) maxCurrent = point.getCurrent();
                if (maxVoltage < point.getVoltage()) maxVoltage = point.getVoltage();
            }
        }

        // 正则化，将所有数值转变为0-1的值
        for (Point point : item.getPoints()) {
            if (point.getCurrent() != null && point.getVoltage() != null) {
                point.setVoltage(point.getVoltage() / maxVoltage);
                point.setCurrent(point.getCurrent() / maxCurrent);
            }
        }
    }

    /**
     * 将全部数据标准化
     *
     * @param list 单相表数据列表
     */
    public void normalizeAll(List<RawDataDO> list) {
        for (RawDataDO item : list) {
            this.normalize(item);
        }
    }

    /**
     * 整流电表数据，直接删点
     *
     * @param list 调整目标数据表
     */
    public void deletePointsToRidNoise(List<RawDataDO> list) {
        // 三人委员会 + 滑动窗口
        for (RawDataDO item : list) {
            // 总表不动
            if (item.getWiringMode().equals("3")) continue;
            int leftFrame = 0;
            int rightFrame = 2;
            while (rightFrame < item.getPoints().size()) {
                int position = rectify(item.getPoints().get(leftFrame), item.getPoints().get(leftFrame + 1), item.getPoints().get(rightFrame++));
                // 判断出修改的位置后，直接将其置空，相当于删除掉该点
                if (position != -1) {
                    item.getPoints().get(position + leftFrame).setCurrent(null);
                    item.getPoints().get(position + leftFrame).setVoltage(null);
                }
                leftFrame++;
            }
        }
    }

    /**
     * 根据分为值坐标计算Q值
     *
     * @param qIndex   分为值坐标，含小数
     * @param currents 电压数据列表
     * @return Q值
     */
    private Double calculateQByIndex(double qIndex, List<Double> currents) {
        return currents.get((int) qIndex) * (qIndex - (int) qIndex) + currents.get((int) qIndex + 1) * (1 - (qIndex - (int) qIndex));
    }

    /**
     * 判断目标值是否为奇异点
     *
     * @param upBound   判断上界
     * @param downBound 判断下界
     * @param target    判断目标
     * @return 判断结果
     */
    private boolean isOutOfBound(Double upBound, Double downBound, Double target) {
        return target > upBound || target < downBound;
    }

    /**
     * 使用箱型图整压，采用剔除值策略，总表和户表都走流程
     *
     * @param list 调整数据列表
     */
    public void boxPlotDeleteToRidNoise(List<RawDataDO> list, boolean deleteOrNot) {
        for (RawDataDO item : list) {
            // 首先获取到所有电压值
            List<Double> currents = new LinkedList<>();

            // 将电压值复制到预备的数组中
            for (Point point : item.getPoints()) {
                if (point.getCurrent() != null) currents.add(point.getCurrent());
            }
            // 电压排序，准备执行箱型图算法
            Collections.sort(currents);
            double n = currents.size();

            // 首先找到下四分位数Q1，其位于(1+n)/4的位置
            double q1 = calculateQByIndex((1 + n) / 4, currents);
            // 找到上四分位数
            double q3 = calculateQByIndex(3 * (1 + n) / 4, currents);
            // 计算上下限
            double upBound = q3 + 1.5 * (q3 - q1);
            double downBound = q1 - 1.5 * (q3 - q1);

            System.out.printf("Box plot: upBound - %f, downBound - %f%n", upBound, downBound);

            // 遍历所有Double值，对于高于上限或低于下限的点，将其该点的值
            if (deleteOrNot) {
                for (Point point : item.getPoints()) {
                    if (point.getCurrent() != null && isOutOfBound(upBound, downBound, point.getCurrent())) {
                        System.out.printf("rip point: %s%n", point);
                        point.setCurrent(null);
                        point.setVoltage(null);
                    }
                }
            } else {
                for (int i = 0; i < item.getPoints().size(); i++) {
                    if (item.getPoints().get(i).getVoltage() != null && isOutOfBound(upBound, downBound, item.getPoints().get(i).getCurrent())) {
                        if (item.getPoints().get(i + 1).getVoltage() != null && item.getPoints().get(i - 1).getVoltage() != null) {
                            item.getPoints().get(i).setVoltage((item.getPoints().get(i + 1).getVoltage() + item.getPoints().get(i - 1).getVoltage()) / 2);
                        }
                    }
                }
            }
        }
    }
}
